#region

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Charisma.Linq;
using Charisma.MongoDb.Linq.Expressions;
using MongoDB.Bson;
using FilterDocument = System.Collections.Generic.Dictionary<string, object>;
using Condition = System.Collections.Generic.KeyValuePair<string, object>;

#endregion

namespace Charisma.MongoDb.Linq
{
    internal class FilterDocumentBuilder : MongoExpressionVisitor
    {
        public static FilterDocument BuildDocument(Expression expression)
        {
            var builder = new FilterDocumentBuilder();
            var e = builder.Visit(expression);
            return e == null ? new FilterDocument() : ToDocument(e).Document;
        }

        private static DocumentExpression ToDocument(Expression e)
        {
            var ne = e as MongoExpression;
            if (ne == null)
                throw new InvalidOperationException();
            if (ne.MongoNodeType == MongoExpressionType.Document)
                return (DocumentExpression) ne;
            if (ne.MongoNodeType == MongoExpressionType.Field && ne.Type.StripNullable() == typeof (bool))
            {
                var fe = (FieldExpression) ne;
                return new DocumentExpression(new FilterDocument {{fe.Name, true}});
            }
            throw new InvalidOperationException();
        }

        protected override Expression VisitConstant(ConstantExpression ce)
        {
            return ce.Value == null 
                ? Expression.Constant(BsonNull.Value, typeof (BsonNull)) 
                : ce;
        }

        protected internal override Expression VisitField(FieldExpression fe)
        {
            return fe;
        }

        protected internal override Expression VisitEntity(EntityExpression ee)
        {
            throw new NotSupportedException();
        }

        protected override Expression VisitBinary(BinaryExpression b)
        {
            var left = Visit(b.Left);
            var right = Visit(b.Right);
            if (right is FieldExpression && !(left is FieldExpression))
            {
                var some = left;
                left = right;
                right = some;
            }

            switch (b.NodeType)
            {
                case ExpressionType.And:
                case ExpressionType.AndAlso:
                    return BuildConjunctionDocument(left, right);
                case ExpressionType.Equal:
                    return new DocumentExpression(new FilterDocument
                                                      {
                                                          {((FieldExpression) left).Name,  ExpressionEvaluator.Evaluate(right).Value}
                                                      });
                case ExpressionType.GreaterThan:
                    return BuildConditionalDocument(left, right, "$gt");
                case ExpressionType.GreaterThanOrEqual:
                    return BuildConditionalDocument(left, right, "$gte");
                case ExpressionType.LessThan:
                    return BuildConditionalDocument(left, right, "$lt");
                case ExpressionType.LessThanOrEqual:
                    return BuildConditionalDocument(left, right, "$lte");
                case ExpressionType.NotEqual:
                    return BuildConditionalDocument(left, right, "$ne");
                case ExpressionType.Modulo:
                    throw new NotImplementedException();
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    return BuildDisjunctionDocument(left, right);
                default:
                    throw new NotSupportedException(string.Format("The operation {0} is not supported.", b.NodeType));
            }
        }

        protected override Expression VisitUnary(UnaryExpression u)
        {
            switch (u.NodeType)
            {
                case ExpressionType.Not:
                    return BuildNegateDocument(u);
                case ExpressionType.ArrayLength:
                    var source = Visit(u.Operand);
                    var fe = source as FieldExpression;
                    if (fe != null)
                    {
//            return new DocumentExpression(new FilterDocument {{fe.Name, true}})
                        throw new NotImplementedException();
                    }
                    goto default;
                case ExpressionType.Convert:
                case ExpressionType.ConvertChecked:
                    return Visit(u.Operand);
                default:
                    throw new NotSupportedException(string.Format("The unary operator {0} is not supported.", u.NodeType));
            }
        }

        protected override Expression VisitMember(MemberExpression m)
        {
            if (m.Member.Name == "Length")
            {
                if (m.Member.DeclaringType == typeof (Array))
                {
                    var source = Visit(m.Expression);
                    var fe = source as FieldExpression;
                    if (fe != null)
                    {
//            return new DocumentExpression(new FilterDocument {{fe.Name, true}})
                        throw new NotImplementedException();
                    }
                }
            }
            else if (m.Member.Name == "Count")
            {
                if (typeof (ICollection).IsAssignableFrom(m.Member.DeclaringType) ||
                    m.Member.DeclaringType.IsOfGenericInterface(typeof (ICollection<>)))
                {
                    var source = Visit(m.Expression);
                    var fe = source as FieldExpression;
                    if (fe != null)
                    {
                        throw new NotImplementedException();
                    }
                }
            }

            throw new NotSupportedException(string.Format("The member {0} is not supported.", m.Member.Name));
        }

        protected override Expression VisitMethodCall(MethodCallExpression m)
        {
            throw new NotSupportedException(string.Format("The method {0} is not supported.", m.Method.Name));
        }


        private Expression BuildNegateDocument(UnaryExpression u)
        {
            var e = Visit(u.Operand);
            var fe = e as FieldExpression;
            if (fe != null)
                return new DocumentExpression(new FilterDocument {{fe.Name, false}});
            var de = e as DocumentExpression;
            if (de != null)
            {
                var document = de.Document;
                if (de.Document.Count == 1)
                {
                    var key = document.Keys.First();
                    var value = document.Values.First();
                    if (key == "$or")
                    {
                        var result = new FilterDocument {{"$nor", value}};
                        return new DocumentExpression(result);
                    }
                    if (key == "$nor")
                    {
                        var result = new FilterDocument {{"$or", value}};
                        return new DocumentExpression(result);
                    }
                }
                var conditionList = new List<FilterDocument>();
                foreach (var pair in document)
                {
                    var key = pair.Key;
                    var value = pair.Value;
                    if (key == "$or" || key == "$nor")
                        throw new NotSupportedException();
                    conditionList.AddRange(BuildNegatedConditionList(key, value));
                }
                var disjunction = new FilterDocument {{"$or", conditionList}};
                return new DocumentExpression(disjunction);
            }
            throw new NotSupportedException();
        }

        private static IEnumerable<FilterDocument> BuildNegatedConditionList(string key, object value)
        {
            var condition = value as Dictionary<string, object>;
            if (condition == null)
            {
                var ne = new FilterDocument {{"$ne", value}};
                return new List<FilterDocument> {new FilterDocument {{key, ne}}};
            }
            var negated = NegateConditions(condition);
            return negated
                .Select(p => new FilterDocument
                                 {
                                     {
                                         key, p.Key == null
                                                  ? p.Value
                                                  : new FilterDocument {{p.Key, p.Value}}
                                         }
                                 })
                .ToList();
        }

        private static IEnumerable<Condition> NegateConditions(IEnumerable<Condition> conditions)
        {
            var result = new List<Condition>();
            foreach (var pair in conditions)
            {
                switch (pair.Key)
                {
                    case "$gt":
                        result.Add(new Condition("$lte", pair.Value));
                        break;
                    case "$gte":
                        result.Add(new Condition("$lt", pair.Value));
                        break;
                    case "$lt":
                        result.Add(new Condition("$gte", pair.Value));
                        break;
                    case "$lte":
                        result.Add(new Condition("$gt", pair.Value));
                        break;
                    case "$ne":
                        result.Add(new Condition(null, pair.Value));
                        break;
                    case "$in":
                        result.Add(new Condition("$nin", pair.Value));
                        break;
                    case "$nin":
                        result.Add(new Condition("$in", pair.Value));
                        break;
                    case "$not":
                        var condition = (FilterDocument) pair.Value;
                        if (condition.Count != 1)
                            throw new NotSupportedException();
                        var conditionElement = condition.First();
                        result.Add(conditionElement);
                        break;
                    default:
                        result.Add(new Condition("$not", new Dictionary<string, object> {{pair.Key, pair.Value}}));
                        break;
                }
            }
            return result;
        }

        private Expression BuildDisjunctionDocument(Expression left, Expression right)
        {
            var leftDocument = ToDocument(left).Document;
            var rightDocument = ToDocument(right).Document;
            if (leftDocument.Count == 1 && rightDocument.Count == 1)
            {
                var leftKey = leftDocument.Keys.First();
                if (rightDocument.ContainsKey(leftKey))
                {
                    if (leftKey == "$nor")
                        throw new NotSupportedException();
                    if (leftKey == "$or")
                    {
                        var leftValue = (List<object>) leftDocument.First().Value;
                        var rightValue = (List<object>) rightDocument.First().Value;
                        var result = new FilterDocument {{"$or", leftValue.Concat(rightValue).ToList()}};
                        return new DocumentExpression(result);
                    }
                    else
                    {
                        var leftValue = leftDocument.First().Value;
                        var rightValue = rightDocument.First().Value;
                        var leftCondition = leftValue as FilterDocument;
                        var rightCondition = rightValue as FilterDocument;
                        if (leftCondition == null && rightCondition == null)
                        {
                            var condition = new FilterDocument {{"$in", new List<object> {leftValue, rightValue}}};
                            var result = new FilterDocument {{leftKey, condition}};
                            return new DocumentExpression(result);
                        }
                        if (leftCondition == null)
                        {
                            var temp = leftValue;
                            leftValue = rightValue;
                            rightValue = temp;
                            leftCondition = (FilterDocument) leftValue;
                            rightCondition = rightValue as FilterDocument;
                        }
                        if (rightCondition == null)
                        {
                            if (leftCondition.Count == 1)
                            {
                                object leftSubValue;
                                if (leftCondition.TryGetValue("$in", out leftSubValue))
                                {
                                    var leftSubList = (List<object>) leftSubValue;
                                    var condition = new FilterDocument
                                                        {{"$in", leftSubList.Concat(new[] {rightValue}).ToList()}};
                                    var result = new FilterDocument {{leftKey, condition}};
                                    return new DocumentExpression(result);
                                }
                            }
                            return BuildDisjunctionDocument(leftDocument, rightDocument);
                        }
                        if (leftCondition.Count == 1 && rightCondition.Count == 1)
                        {
                            object leftSubValue;
                            object rightSubValue;
                            if (leftCondition.TryGetValue("$in", out leftSubValue) &&
                                rightCondition.TryGetValue("$in", out rightSubValue))
                            {
                                var leftSubList = (List<object>) leftSubValue;
                                var rightSubList = (List<object>) rightSubValue;
                                var condition = new FilterDocument {{"$in", leftSubList.Concat(rightSubList).ToList()}};
                                var result = new FilterDocument {{leftKey, condition}};
                                return new DocumentExpression(result);
                            }
                        }
                        return BuildDisjunctionDocument(leftDocument, rightDocument);
                    }
                }
                var rightKey = rightDocument.Keys.First();
                if (leftKey == "$nor" || rightKey == "$nor")
                    throw new NotSupportedException();
                if (leftKey == "$or" || rightKey == "$or")
                {
                    List<object> value;
                    if (leftKey == "$or")
                    {
                        var leftValue = (List<object>) leftDocument.First().Value;
                        value = new List<object>(leftValue) {rightDocument};
                    }
                    else
                    {
                        var rightValue = (List<object>) rightDocument.First().Value;
                        value = new List<object>(rightValue) {leftDocument};
                    }
                    var result = new FilterDocument {{"$or", value}};
                    return new DocumentExpression(result);
                }
                return BuildDisjunctionDocument(leftDocument, rightDocument);
            }
            if (leftDocument.Count == 1)
            {
                if (leftDocument.ContainsKey("$nor") || rightDocument.ContainsKey("$or") ||
                    rightDocument.ContainsKey("$nor"))
                    throw new NotSupportedException();
                var leftKey = leftDocument.Keys.First();
                if (leftKey == "$or")
                {
                    var leftValue = (List<object>) leftDocument.First().Value;
                    var value = new List<object>(leftValue) {rightDocument};
                    var result = new FilterDocument {{"$or", value}};
                    return new DocumentExpression(result);
                }
            }
            else if (rightDocument.Count == 1)
            {
                if (rightDocument.ContainsKey("$nor") || leftDocument.ContainsKey("$or") ||
                    leftDocument.ContainsKey("$nor"))
                    throw new NotSupportedException();
                var rightKey = rightDocument.Keys.First();
                if (rightKey == "$or")
                {
                    var rightValue = (List<object>) rightDocument.First().Value;
                    var value = new List<object>(rightValue) {rightDocument};
                    var result = new FilterDocument {{"$or", value}};
                    return new DocumentExpression(result);
                }
            }
            else
            {
                if (leftDocument.ContainsKey("$or") || leftDocument.ContainsKey("$nor") ||
                    rightDocument.ContainsKey("$or") || rightDocument.ContainsKey("$nor"))
                    throw new NotSupportedException();
            }
            return BuildDisjunctionDocument(leftDocument, rightDocument);
        }

        private Expression BuildDisjunctionDocument(FilterDocument leftDocument, FilterDocument rightDocument)
        {
            var disjunction = new FilterDocument {{"$or", new List<object> {leftDocument, rightDocument}}};
            return new DocumentExpression(disjunction);
        }

        private Expression BuildConjunctionDocument(Expression left, Expression right)
        {
            var leftDocument = ToDocument(left).Document;
            var rightDocument = ToDocument(right).Document;
            var result = new FilterDocument(leftDocument);
            foreach (var pair in rightDocument)
            {
                object leftValue;
                if (result.TryGetValue(pair.Key, out leftValue))
                {
                    var leftCondition = leftValue as FilterDocument;
                    var rightCondition = pair.Value as FilterDocument;
                    if (leftCondition != null && rightCondition != null)
                        foreach (var condition in rightCondition)
                            leftCondition[condition.Key] = condition.Value;
                    else if (pair.Key == "$nor")
                    {
                        var leftList = (List<object>) leftValue;
                        var rightList = (List<object>) pair.Value;
                        result["$nor"] = leftList.Concat(rightList).ToList();
                    }
                    else
                        throw new NotSupportedException();
                }
                else
                    result[pair.Key] = pair.Value;
            }
            return new DocumentExpression(result);
        }

        private static DocumentExpression BuildConditionalDocument(Expression left, Expression right, string @operator)
        {
            var key = ((FieldExpression) left).Name;
            var condition = new FilterDocument {{@operator, ExpressionEvaluator.Evaluate(right).Value}};
            var result = new FilterDocument {{key, condition}};
            return new DocumentExpression(result);
        }
    }
}